Skip to content

Python: wrap Anthropic and Gemini SDK exceptions in ChatClientException - #7855

Open
karthikchundi-commits wants to merge 2 commits into
microsoft:mainfrom
karthikchundi-commits:fix/anthropic-gemini-wrap-sdk-exceptions
Open

Python: wrap Anthropic and Gemini SDK exceptions in ChatClientException#7855
karthikchundi-commits wants to merge 2 commits into
microsoft:mainfrom
karthikchundi-commits:fix/anthropic-gemini-wrap-sdk-exceptions

Conversation

@karthikchundi-commits

Copy link
Copy Markdown

Summary

_inner_get_response() in the OpenAI, Mistral, Ollama, and Bedrock chat clients all translate raw provider SDK exceptions into the framework's ChatClientException hierarchy (ChatClientInvalidAuthException for auth failures, ChatClientInvalidRequestException for bad requests, ChatClientException otherwise). Mistral's test suite explicitly asserts this behavior (test_get_response_http_error_wrapped, test_get_response_network_error_wrapped).

The Anthropic and Gemini clients call their SDKs directly with no try/except at all, so a raw anthropic.APIError or google.genai.errors.APIError (and subclasses) propagates unwrapped instead. Code written against the framework's provider-agnostic abstraction - catching ChatClientException to handle "any chat client failure," which is the documented purpose of that base class - silently fails to catch failures from these two providers specifically.

Fix

Wraps both the streaming and non-streaming call sites in both RawAnthropicClient._inner_get_response and RawGeminiChatClient._inner_get_response, classifying auth vs. bad-request vs. other errors the same way Mistral's existing implementation already does:

  • Anthropic: anthropic.AuthenticationErrorChatClientInvalidAuthException, anthropic.BadRequestErrorChatClientInvalidRequestException, any other anthropic.APIErrorChatClientException.
  • Gemini: google.genai.errors.ClientError with code == 401ChatClientInvalidAuthException, other ClientErrorChatClientInvalidRequestException, any other google.genai.errors.APIError (including ServerError) → ChatClientException.

I verified both SDKs' actual exception hierarchies against the installed anthropic (0.103.1) and google-genai packages rather than assuming from memory, since anthropic.InternalServerError in particular doesn't carry a class-level status_code the way AuthenticationError/BadRequestError do.

Testing

Added regression tests mirroring Mistral's existing coverage pattern:

  • test_anthropic_client.py: test_inner_get_response_wraps_sdk_errors (parametrized over AuthenticationError/BadRequestError/InternalServerError) + test_inner_get_response_streaming_wraps_sdk_errors.
  • test_gemini_client.py: test_get_response_wraps_sdk_errors (parametrized over 401/400 ClientError + ServerError) + test_get_response_streaming_wraps_sdk_errors.

Ran locally (editable install of agent-framework-core, -anthropic, and -gemini, pytest, Python 3.13 via conda):

  • packages/anthropic: full suite passes, 162 tests, no regressions.
  • packages/gemini: full suite passes, 154 tests, no regressions.
  • ruff format + ruff check (pinned ruff==0.16.3) clean on all four changed files.

I don't have a way to hit the live Anthropic or Gemini APIs in this environment, so verification is at the unit level (real SDK exception classes, mocked HTTP layer) rather than an end-to-end run against production endpoints - happy to have this validated further in CI/review.

Scope note

Per CONTRIBUTING.md's guidance to discuss non-trivial changes first: this felt like a "small, obvious fix" (matching an existing, already-tested pattern used by 4 of 6 sibling providers, no new API surface, no behavioral change to successful responses) rather than something needing a design discussion, so I went straight to a PR. Happy to file an issue first if that's preferred for something touching this many provider packages.

_inner_get_response() in the OpenAI, Mistral, Ollama, and Bedrock chat
clients all translate raw provider SDK exceptions into the framework's
ChatClientException hierarchy (ChatClientInvalidAuthException for auth
failures, ChatClientInvalidRequestException for bad requests,
ChatClientException otherwise). Mistral's test suite explicitly asserts
this behavior (test_get_response_http_error_wrapped,
test_get_response_network_error_wrapped).

The Anthropic and Gemini clients call their SDKs directly with no
try/except at all, so a raw anthropic.APIError or google.genai.errors.APIError
(and subclasses) propagates unwrapped. Code that catches
ChatClientException to handle chat-client failures in a
provider-agnostic way - the documented purpose of that base class -
silently fails to catch failures from these two providers.

Wraps both the streaming and non-streaming call sites in both clients,
using each SDK's real exception hierarchy (verified against the
installed anthropic and google-genai packages, not assumed) to
classify auth vs. bad-request vs. other errors the same way Mistral
already does.

Adds regression tests mirroring Mistral's existing coverage: one
parametrized non-streaming test per provider covering all three
exception classes, plus a streaming-path test per provider. Full
anthropic and gemini unit test suites pass locally (162 and 154 tests
respectively, no regressions).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Normalizes Anthropic and Gemini SDK failures into the framework’s chat-client exception hierarchy.

Changes:

  • Wraps streaming and non-streaming provider errors.
  • Adds regression tests for error classification.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
python/packages/gemini/agent_framework_gemini/_chat_client.py Adds Gemini exception translation.
python/packages/gemini/tests/test_gemini_client.py Tests Gemini error wrapping.
python/packages/anthropic/agent_framework_anthropic/_chat_client.py Adds Anthropic exception translation.
python/packages/anthropic/tests/test_anthropic_client.py Tests Anthropic error wrapping.
Suppressed comments (2)

python/packages/gemini/agent_framework_gemini/_chat_client.py:597

  • A 403 from Gemini/Vertex represents an authentication/authorization failure, not an invalid request. Include it with 401 here so the non-streaming path emits ChatClientInvalidAuthException consistently with the framework's existing provider mappings.
                if ex.code == 401:

python/packages/anthropic/agent_framework_anthropic/_chat_client.py:594

  • The non-streaming path maps Anthropic's 403 PermissionDeniedError to the generic base exception. Classify 403 as ChatClientInvalidAuthException so callers can handle credential and permission failures consistently across providers and response modes.
            except AnthropicAPIError as ex:
                raise ChatClientException(f"Anthropic chat request failed: {ex}", inner_exception=ex) from ex

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +581 to +582
except GenAIAPIError as ex:
raise ChatClientException(f"Gemini chat request failed: {ex}", inner_exception=ex) from ex
Comment on lines +602 to +603
except GenAIAPIError as ex:
raise ChatClientException(f"Gemini chat request failed: {ex}", inner_exception=ex) from ex
):
yield self._process_chunk(chunk)
except GenAIClientError as ex:
if ex.code == 401:
Comment on lines +577 to +578
except AnthropicAPIError as ex:
raise ChatClientException(f"Anthropic chat request failed: {ex}", inner_exception=ex) from ex
Comment on lines +411 to +413
mock.aio.models.generate_content_stream = AsyncMock(
side_effect=genai_errors.ClientError(401, {"error": {"message": "invalid api key"}})
)
Comment on lines +1864 to +1866
mock_anthropic_client.beta.messages.create.side_effect = _anthropic_status_error(
anthropic_sdk.AuthenticationError, 401, "invalid api key"
)
@karthikchundi-commits

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@eavanvalkenburg

Copy link
Copy Markdown
Member

karthikchundi-commits this looks good overall, but some valid comments by the reviews that need to be addressed, please have a look!

@github-actions

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/anthropic/agent_framework_anthropic
   _chat_client.py5563893%478, 481, 568, 573–574, 577–578, 724, 824, 858, 948, 987–988, 1066, 1068, 1098–1099, 1183–1185, 1189–1191, 1195–1198, 1233, 1349, 1359, 1411, 1559–1560, 1577, 1590, 1603, 1628–1629
packages/gemini/agent_framework_gemini
   _chat_client.py5102096%421, 580–582, 776, 816–817, 826–827, 830–831, 863, 870, 995, 1006, 1198–1199, 1203, 1214–1215
TOTAL48192449990% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
9737 36 💤 0 ❌ 0 🔥 2m 33s ⏱️

…er mid-stream errors

- Map HTTP 403 (Anthropic PermissionDeniedError / Gemini ClientError) to
  ChatClientInvalidAuthException, matching Mistral's 401/403 handling.
- Replace the provider-specific except chain with a catch-all that routes
  every non-framework exception through a _wrap_*_error helper, so transport
  failures and credential-refresh errors no longer escape ChatClientException.
- Re-raise AgentFrameworkException (e.g. ContentError) untouched.
- Extend tests to cover 403, a non-APIError fallback, and a failure raised
  partway through iterating the stream (generator yields then raises).
@karthikchundi-commits

Copy link
Copy Markdown
Author

Thanks Eduard van Valkenburg (@eavanvalkenburg). Pushed 652c3d0 addressing the review comments:

  • 403 -> auth: AnthropicPermissionDeniedError and Gemini 403 ClientError now map to ChatClientInvalidAuthException, matching Mistral's 401/403 handling.
  • Non-APIError failures: replaced the provider-specific except chain with a catch-all that routes everything through a _wrap_{anthropic,gemini}_error helper, so transport errors, timeouts, and Vertex credential-refresh failures are wrapped in ChatClientException too instead of escaping raw. AgentFrameworkException (e.g. ContentError) is re-raised untouched.
  • Mid-stream failures: streaming tests now also exercise a failure raised partway through iterating the stream (async generator yields a chunk, then raises), not just a failure on the initial call.
  • Fixed the pyright failure in Package Checks (the generate_content call now goes through the same cast the streaming path already uses, dropping the stale # type: ignore).

Local: poe pyright/test-typing clean for both packages, poe test -P anthropic 160 passed, poe test -P gemini 152 passed, poe syntax -C clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants